package sketch.main;

import gov.nasa.jpf.symbc.SolvedPCResults;

import java.io.File;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import junit.framework.TestCase;

import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.BodyDeclaration;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.TypeDeclaration;

import randoop.util.Files;
import sketch.ast.ASTUtils;
import sketch.instrumenter.SourceCodeInstrumenter;
import sketch.instrumenter.Tracer;
import sketch.instrumenter.TracerFileReader;
import sketch.jpf.symbc.SymbolicJPF;
import sketch.ounit.OUnit;
import sketch.ounit.ObservationInstantiationVisitor;
import sketch.specs.MethodDumperAfterRemovingUnusedVars;
import sketch.specs.RedundantParenthesesRemover;
import sketch.specs.SketchedMethodDeclaration;
import sketch.specs.SymbolicFileParser;
import sketch.specs.SymbolicValueReplacement;
import sketch.specs.annotation.Repl;
import sketch.specs.annotation.Repls;
import sketch.specs.annotation.TestSketch;
import sketch.specs.symbc.SymbolicExecDriverCreator;
import sketch.util.Checker;
import sketch.util.Globals;
import sketch.util.IntermediateFiles;
import sketch.util.SketchLog;
import sketch.util.SketchUtil;

/**
 * A utility method to make life easier
 * */
public class GeneratingTests extends TestCase {
	
	public static boolean release = false;
	
	/**
	 * The temp dir for storing instrumented file, and symbolic test driver.
	 * This dir MUST be in classpath.
	 * */
	//public static String tmp_dir_instrumentation = "./output/";
	
	/**
	 * Given a test sketch, as well as its corresponding compilation unit, this method first expands
	 * all test sketch, and then execute it to obtain runtime information, e.g. the symbolic value
	 * after solving and testing oracle value.
	 * XXX  this method is for experiment, will be obsoleted soon!
	 * */
	public static void generateTestsAndWriteToFile(List<SketchedMethodDeclaration> sketchedMethods, CompilationUnit unit,
			String outputFileDir, String packageName, String outputClassName) {
		Checker.checkNull(packageName, "The package name could not be null.");
		Checker.checkTrue(!packageName.trim().equals(""), "The package name could not be empty.");
		//the output generated method
		List<MethodDeclaration> generatedMethod = new LinkedList<MethodDeclaration>();
		for(SketchedMethodDeclaration sketchedMethod : sketchedMethods) {
			List<MethodDeclaration> expanded = sketchedMethod.getMethodsAfterExpansion();
			//System.out.println(expanded);
			//System.out.println(expanded.size());
			for(MethodDeclaration expand : expanded) {
				//System.out.println(expand);
				//for each method generated from expansion, we removed all unused
				//variables, make the method public, and add the OUnit annotation
				MethodDumperAfterRemovingUnusedVars dumper = new MethodDumperAfterRemovingUnusedVars(expand);
				dumper.dumpMethodWithUnusedVarRemoved();
				ASTUtils.makeMethodPublic(expand);
				ASTUtils.addOUnitAnnotation(expand);
				//System.out.println(expand);
			}
			generatedMethod.addAll(expanded);
		}
		//an assumption here, the unit has 1 class. only for the experiment study here.
		assertEquals(1, unit.types().size());
		//get the type, and add methods
		TypeDeclaration typeDeclaration = (TypeDeclaration)unit.types().get(0);
		
		//add all generated (expanded) methods
		List<BodyDeclaration> bodyDeclarations = typeDeclaration.bodyDeclarations();
		bodyDeclarations.addAll(generatedMethod);
		//set the type (class) name, as well as the package name
		ASTUtils.setTypeName(typeDeclaration, outputClassName);
		ASTUtils.setPackageDeclaration(unit, packageName);
		//remove all test sketches after expansion
		ASTUtils.removeTestSketches(unit);
		
		//try to remove redundant parenthesis
		RedundantParenthesesRemover<CompilationUnit> remover = new RedundantParenthesesRemover<CompilationUnit>(unit);
		try  {
		   unit = remover.getResult();
		} catch (RuntimeException e) {
			System.err.println("Error occurs in removing redundant { }, simply ignore that.");
		}
		//do symbolic execution, and run the program on the fly to obtain dynamic information
		genSymbolicValuesToOutputTests(unit, outputFileDir, outputClassName, packageName, new String[]{},
				null, null);
	}
	
	/**
	 * This method is doing two things:
	 * First, it generates test drivers, do symbolic execution, and replace symbolic value with concrete value
	 * Second, it run the code concretely to obtain runtime information, then replace the original placeholder
	 *        testing oracles with runtime values.
	 * 
	 * NOTE that this method will only process MethodDeclaration decorated with OUnit
	 * */
	public static File genSymbolicValuesToOutputTests(CompilationUnit unit, String outputFileDir,
			String outputFileName, String packageName, String[] extraImports, Class<?> symbolicClass, String methodName) {
		SketchLog.configDefaultLog();
		
		//configure the dir
		String tmp_dir_instrumentation = SketchTestOptions.output_dir;
		
		//need to change the packageName
		/**
		 * Do symbolic execution to get the value
		 * Up to now:
		 * 1. unit -> an untouched compilation unit
		 * steps to performance symbolic execution.
		 * 1.  create symbolic execution drivers
		 * 2.  call jpf to parse the results
		 * 3.  add to the unit, note that get the values from bottom-up
		 * */
		//System.err.println("package name: " + unit.getPackage().getName().toString() + ",  package name: " + packageName);
		if(packageName != null && (unit.getPackage() == null || !unit.getPackage().getName().toString().equals(packageName))) {
		  ASTUtils.setPackageDeclaration(unit, packageName);
		}
		//clear the trace file
		clear_trace_file();
		
		//a conservative way
		CompilationUnit unit_for_symbc = ASTUtils.deepClone(unit);
		List<MethodDeclaration> method_decls = ASTUtils.getAllMethods(unit_for_symbc);
		for(MethodDeclaration method_decl : method_decls) {
			//add OUnit annotation for every method declaration?
			ASTUtils.addOUnitAnnotation(method_decl);
		}
		//find all method declaration that declares a symbolic value
		SymbolicExecDriverCreator creator = new SymbolicExecDriverCreator(unit_for_symbc);
		List<MethodDeclaration> methods_need_driver = creator.getMethodDeclarationsForTestDriver();
		//skip if there is no symbolic execution phase needed
		boolean hasSetUp = creator.hasSetUp();
		boolean hasTearDown = creator.hasTearDown();
		if(!methods_need_driver.isEmpty()) {
			//if symbolic execution is needed, the symbolic class and the symbolic method could
			//not be null
			Checker.checkNull(symbolicClass, "The symbolic class could not be null, use --symbolic_class to specify it.");
			Checker.checkNull(methodName, "The method name could not be null, use --symbolic_method to specify it");
			
			//replace symbolic execution with normal values
			String dirStructure = (packageName == null) ? "" : SketchUtil.transClassNameDotToSlash(packageName);
			String dir = tmp_dir_instrumentation + Globals.fileSep + dirStructure;
			SketchUtil.mkdirs(dir);
			//must include the driver itself, the name has been changed inside SymbolicExecDriverCreator
			String selfPath = dir + Globals.fileSep + outputFileName +  SymbolicExecDriverCreator.SYMBOLIC + ".java";
			Files.writeToFileNoException(unit_for_symbc.toString(), selfPath);
			File self = new File(selfPath);
			//CodeToolkit.compile(self);
			//compile the code, including the driver
			List<File> drivers = creator.createExecDriver(extraImports, hasSetUp, hasTearDown);
			drivers.add(self);
			CodeToolkit.compile(drivers);
			
			//add to intermediate files
			for(File driver : drivers) {
				IntermediateFiles.addIntermediateFile(driver);
				String fullPath = driver.getAbsolutePath();
				int lastFileSep = fullPath.lastIndexOf(Globals.fileSep);
				if(lastFileSep != -1) {
				  String parent_dir = fullPath.substring(0, lastFileSep);
				  IntermediateFiles.addAllFilesAsIntermediates(parent_dir, ".class");
				}
			}

			//do symbolic execution here
			drivers.remove(self);
			for(File driver : drivers) {
			  //load each test driver, the default package is null
			  Class<?> clazz = CodeToolkit.loadClassFromDir(tmp_dir_instrumentation,
					  SketchUtil.transJavaFileNameToClassName(driver.getName(), null));
			  
			  //add to intermediate files, the compiled class file of driver
//			  IntermediateFiles.addIntermediateFile(new File(tmp_dir_instrumentation + Globals.fileSep + SketchUtil.transClassNameDotToSlash(clazz.getName()) + ".class"));
			  IntermediateFiles.addAllFilesAsIntermediates(tmp_dir_instrumentation, ".class");

			  Checker.checkTrue(SolvedPCResults.solvedIntValues.isEmpty() && SolvedPCResults.solvedRealValues.isEmpty(),
					  "Please clear the solved constraints.");
			  
			  SymbolicJPF.execute(clazz, symbolicClass, methodName);
			  //here is the result
			  List<Map<String, Integer>> solvedIntValues = SolvedPCResults.solvedIntValues;
			  List<Map<String, Double>> solvedRealValues = SolvedPCResults.solvedRealValues;
//			  
//			  SketchLog.logln("Symbolic execution file: " + clazz.getName());
//			  SketchLog.logln("  result size: " + solvedIntValues.size());
//			  SketchLog.logln("  all results: " + solvedIntValues);
			  
			  MethodDeclaration findMethodDeclaratinon = null;
			  for(MethodDeclaration m : methods_need_driver) {
				  String name = m.getName().toString();
				  if(driver.getName().indexOf(name) != -1) {
					  findMethodDeclaratinon = m;
					  break;
				  }
			  }
			  
			  Checker.checkNull(findMethodDeclaratinon, "Should be corresponding");
//			  AbstractTypeDeclaration type = ASTUtils.getParent(findMethodDeclaratinon, AbstractTypeDeclaration.class);
			  AbstractTypeDeclaration type = creator.getTypeDeclarationForMethod(findMethodDeclaratinon);
			  Checker.checkNull(type, "Type should not be null.");
			  List<MethodInvocation> invokes = creator.getSymbolicValuesForMethod(findMethodDeclaratinon);
			  List<List<Integer>> int_values = SymbolicFileParser.convertToNestedList(solvedIntValues);
			  List<List<Double>> real_values = SymbolicFileParser.convertToNestedList(solvedRealValues);
			  
			  int count = 0; //XXX error in counting? who knows, there is a tiny bug here
			  boolean noResult = (solvedIntValues.size() == 1) && (solvedIntValues.get(0).isEmpty());
			  if(!noResult && !solvedIntValues.isEmpty()) {
			    SymbolicValueReplacement replacement = new SymbolicValueReplacement(findMethodDeclaratinon, invokes, int_values, real_values);
			    List<MethodDeclaration> replaced = replacement.replaceValues();
			    for(MethodDeclaration m : replaced) {
			    	m.getName().setIdentifier(m.getName().toString() + "_symb_" + System.currentTimeMillis() + "_" + (count++));
			    }
			    type.getName().setIdentifier(SymbolicExecDriverCreator.removeSymbolic(type.getName().toString()));
			    type.bodyDeclarations().addAll(replaced);
			    //remove the original method declaration with symbolic values
			    type.bodyDeclarations().remove(creator.getOriginalMethodDeclaration(findMethodDeclaratinon));
			  }
			  
			  //clear the solved constrains, and ready for the next round
			  SolvedPCResults.reset();
			}
		}
		
		if(!methods_need_driver.isEmpty()) {
		  unit = unit_for_symbc;
		}
		
		/**
		 * In the following code, we need:
		 * 1. if need to obtain some runtime information, we instrument the code, and put the instrumented
		 *    code to a ./output folder (which MUST be in classpath)
		 * 2. compile the instrumented file
		 * 3. run the instrumented file to get the runtime value
		 * 4. read the runtime value, and instantiate all observer values, and finally rewrite the compilation
		 *    unit, and output to the outputdir
		 * */
		
		String outputFileAbsolutePath = outputFileDir + Globals.fileSep + outputFileName + ".java";
		
		//in implementation, we first try to instrument every compilation unit
		//if we found some statements have been inserted, we know we need to get some runtime information
		CompilationUnit instrumented_copy = ASTUtils.deepClone(unit);
		//CompilationUnit uninstrumented_backup = ASTUtils.deepClone(unit);
		//do the instrumentation
		SourceCodeInstrumenter instrumenter = new SourceCodeInstrumenter(instrumented_copy);
		instrumenter.doInstrumentation();
		boolean hasInstrumented = instrumenter.hasInstrumentedTheCode();
		File instrumented_file = null;
		if(hasInstrumented) {
			String dirStructure = (packageName == null) ? "" : SketchUtil.transClassNameDotToSlash(packageName);
			SketchUtil.mkdirs(tmp_dir_instrumentation + Globals.fileSep + dirStructure);
			String instrumentation_dir = tmp_dir_instrumentation + Globals.fileSep + dirStructure
			    + Globals.fileSep + outputFileName + SourceCodeInstrumenter.INSTRUMENTED + ".java";
			//output the instrumented
			instrumented_file = new File(instrumentation_dir);
			ASTUtils.outputUnit(instrumented_copy, instrumented_file);
			//add to intermediate file
			IntermediateFiles.addIntermediateFile(instrumented_file);
		}
		
		//output the uninstrumented one
		
		
		//to execute the program and find the results
		if(hasInstrumented) {
			Checker.checkNull(instrumented_file, "The instrumented_file could not be null.");
			//Tracer.clearFile();
			
			List<File> sources = new LinkedList<File>();
			sources.add(instrumented_file);
			CodeToolkit.compile(sources);
			
			
			String clazzName = (packageName == null) ?  (outputFileName + SourceCodeInstrumenter.INSTRUMENTED) :
				(packageName + "." + outputFileName + SourceCodeInstrumenter.INSTRUMENTED);
			System.out.println("Look up JUnit class, and execute JUnit: " + clazzName);
			Class<?> testClass = CodeToolkit.loadClassFromDir(tmp_dir_instrumentation , clazzName);
			junit.textui.TestRunner.run(testClass);
			
			//add the compiled class file of the instrumented file
//			IntermediateFiles.addIntermediateFile(new File(tmp_dir_instrumentation + Globals.fileSep + SketchUtil.transClassNameDotToSlash(clazzName) + ".class"));
			IntermediateFiles.addAllFilesAsIntermediates(tmp_dir_instrumentation, ".class");
			
			System.out.println("Finish executing JUnit tests, and start rewriting!");
			
			TracerFileReader reader = new TracerFileReader(Tracer.saveFile);
			Map<String, Map<Integer, String>> parsingResults = reader.parse();
			
			IntermediateFiles.addIntermediateFile(Tracer.saveFile);
			
			if(SketchTestOptions.verbose) {
			  System.out.println(parsingResults);
			}
			
			//replace the oracle with concrete values
			ObservationInstantiationVisitor visitor = new ObservationInstantiationVisitor(unit, parsingResults); 
			unit.accept(visitor);
			//replace it, output the final version
//			ASTUtils.outputUnit(unit, outputFileAbsolutePath);
		}
		
		//we need to remove all SketchTest specific annotations
		if(release) {
			ASTUtils.removeAllSketchTestNotationsAndImports(unit);
			List<MethodDeclaration> methods = ASTUtils.getAllMethods(unit);
			for(MethodDeclaration method : methods) {
				ASTUtils.removeMethodAnnotation(method, TestSketch.class);
				ASTUtils.removeMethodAnnotation(method, OUnit.class);
				ASTUtils.removeMethodAnnotation(method, Repls.class);
				ASTUtils.removeMethodAnnotation(method, Repl.class);
			}
		}
		
		//output the result here
		//ASTUtils.removeAllSketchTestNotationsAndImports(unit);
		System.out.println("Write to: " + outputFileAbsolutePath);
		IntermediateFiles.retainIntermediateFile(outputFileAbsolutePath);
		return ASTUtils.outputUnit(unit, outputFileAbsolutePath);
	}
	
	//clear the traceFile.txt
	private static void clear_trace_file() {
		Tracer.clearFile();
	}
}